---
id: snabbdom
title: snabbdom部分源码
---

调试对于搞懂源码是非常重要！

## 前期准备工作

此篇文档所使用的软件版本：

- snabbdom@3.4.0
- webpack@5
- webpack-cli@4
- node@16.3.0

1. 开始

```
// 先创建node项目：

  npm init

// 然后：
  cd my-snabbdom-demo
  npm i snabbdom@3.4.0 -S
  npm i webpack@5 webpack-cli@4 -D

// 新建index.html  和index.js，webpack.config.js 文件，内容如下
```

index.html

```html
<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>webpack</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./dist/main.js"></script>
  </body>
</html>
```

index.js

```js
// 示例代码1
import { init, h, thunk, vnode } from "snabbdom";
// import "./style.css";

// patch 函数用于比较两个vnode的差异，渲染到DOM上
const patch = init([]);

// h 函数(用于生成html的函数)用于生成vnode
let vNode = h("div.container", "hello snabbdom");

debugger;

setTimeout(() => {
  patch(document.querySelector("#app"), vNode); // 进行内容对比，有差异则会替换
}, 3500);

// setTimeout(() => {
//   patch(update, h("!")); //正确清空元素方法
//   // patch(update, null); // 会报错
// }, 3500);
```

webpack.config.js

```js
const path = require("path");
// const TerserWebpackPlugin = require("terser-webpack-plugin"); // 压缩js 代替uglify 因为uglifyjs不支持es6语法，所以用terser-webpack-plugin替代uglifyjs-webpack-plugin

module.exports = {
  // mode: 'development',
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js",
  },
  optimization: {
    minimize: false,
    // minimizer: [new TerserWebpackPlugin()],
  },
};
```

package.json 添加如下命令

```json
"scripts": {
    "build": "webpack-cli ./index.js",
  }

```

运行`npm run build` 打开 index.html 开启控制台进行调试吧。

**运行效果如下**

hello snabbdom

页面元素长成这个样子 ↓

```html
<div class="container">hello snabbdom</div>
```

## snabbdom 项目目录介绍

```
.
|—— src
|    |—— helpers
|    |      └─── class.tsattachto
|    |—— modules
|    |      |—— attributes.ts
|    |      |—— class.ts
|    |      |—— dataset.ts
|    |      |—— eventlisteners.ts
|    |      |—— module.ts
|    |      |—— props.ts
|    |      └─── style.ts
|    |—— h.ts               // h(）函数，用来创建 VNode
|    |—— hooks.ts           // 所有钩子函数的定义
|    |—— htmldomapi.ts      // 对 DOM API 的包装
|    |—— is.ts              // 判断数组和原始值的函数
|    |—— jsx-global.d.ts    // jsx 的类型声明文件
|    |—— jsx.ts             // 处理 jsx
|    |—— snabbdom.bunle.ts  // 入口，己经注册了模块
|    |—— snabbdom.ts        // 初始化，返回 init/h/thunk
|    |—— thunk.ts           // 优化处理，对复杂视图不可变值的优化
└─────── tovnode.ts         // DOM 转换成 VNode
```

## 模块介绍

- attributes  
  设置 DOM 元素的属性，使用 setAttribute()  
  处理布尔类型的属性

- props  
  •和 attributes 模块相似，设置 DOM 元素的属性 element[attr] = value  
  •不处理布尔类型的属性

- class  
  切换类样式  
  •注意：给元素设置类样式是通过 sel 选择器

- dataset  
  设置 data-\* 的自定义属性

- eventlisteners  
  注册和移除事件

- style  
  设置行内样式，支持动画
  delayed/remove/destroy

给 示例代码 1 加点模块功能

```js
// 示例代码2

import { init, h, styleModule, eventListenersModule } from "snabbdom";

const patch = init([styleModule, eventListenersModule]);

let vNode = h(
  "div.container",
  {
    style: {
      color: "green",
    },
    on: {
      click: () => alert("click"),
    },
  },
  "hello snabbdom"
);

let app = document.querySelector("#app");

let update = patch(app, vNode);
```

## 核心方法

### h 函数

h 函数的作用是用来生成 vnode

h 函数最早出现在 hyperScript 中，在里面用来创建超文本标记语言 HTML(HyperText Markup Language)。  
也就是说是用 js 来描述 Hmtl 的函数

```
Create HyperText with JavaScript, on client or server
```

在 vue 中也可以使用 h 函数

```html
<!-- 示例代码3 -->

<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>h函数</title>
    <!-- 1.引用vueJS -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script>
    // 2. 使用Vue构造函数
    new Vue({
      el: "#app",
      render: (h) => h("div", "hello h函数"),
    });
  </script>
</html>
```

我们来看一下 snabbdom 中的 h 函数的方法体

```js
// h函数位置 node_module/snabbdom/src/h.ts

// 有3个形参，其中b和c带问号，说明使用时可以不传，返回值是VNode。这是一个ts接口
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {};
  let children: any;
  let text: any;
  let i: number;
  // 1. 进入这个判断说明C有值
  if (c !== undefined) {
    // 1.1 判断b不为空的话，b的值赋给data。
    if (b !== null) {
      data = b;
    }
    // 1.2 开始判断第三个参数c的类型：如果是数组的话，赋值给children
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) {
      // primitive方法 判断是否是字符串或数字
      text = c.toString();
    } else if (c && c.sel) {
      // c存在 并且有个sel的属性， 就赋值给children
      children = [c];
    }
    // 2. 判断第二个参数是否有值
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) {
      text = b.toString();
    } else if (b && b.sel) {
      children = [b];
    } else {
      data = b;
    }
  }

  //3.判断children 是否有值，如果有值肯定是个数组。
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i]))
        children[i] = vnode(
          undefined,
          undefined,
          undefined,
          children[i],
          undefined
        );
    }
  }
  //4. 如果是svg 添加命名空间
  if (
    sel[0] === "s" &&
    sel[1] === "v" &&
    sel[2] === "g" &&
    (sel.length === 3 || sel[3] === "." || sel[3] === "#")
  ) {
    addNS(data, children, sel);
  }

  // 5.最后返回 vnode 函数
  return vnode(sel, data, children, text, undefined);
}
```

### vnode 函数

```js
export function vnode(
  sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | DocumentFragment | Text | undefined
): VNode {
  const key = data === undefined ? undefined : data.key;
  // 返回结果是个js 对象。这个对象结构就是snabbdom内的vnode结构
  return { sel, data, children, text, elm, key };
}
```

### patch 函数

patch 函数 是对比两个 vnode 的差异，并更新到 DOM 上。

```js
// path函数是init函数的返回值。
return function patch(
    oldVnode: VNode | Element | DocumentFragment,
    vnode: VNode
  ): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    //1. 执行pre钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 2. 判断第一个参数的类型。如果属于原生的DOM,就转成空的vnode
    if (isElement(api, oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);

      // 2.1 这个判断条件原先是没有的,新加的, 判断是否是DocumentFragment，是的话就给它赋值为空。
    } else if (isDocumentFragment(api, oldVnode)) {
      oldVnode = emptyDocumentFragmentAt(oldVnode);
    }

    // 3. 判断两个vnode是否相同,判断的依据是新旧节点的(key,sel)
    if (sameVnode(oldVnode, vnode)) {
      //3.1 找到差异并更新DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue);

    // 4.进入else说明新旧节点不同
    } else {

      // 4.1 找到旧节点父节点
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;

      // 4.2 给新节点创建DOM, 触发init/create钩子函数
      createElm(vnode, insertedVnodeQueue);

      // 4.3 插入创建的DOM并删除旧的元素。
      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
    // 5 执行insert钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }


    // 6把当前的vnode 返回，可用于下次 vnode比较。
    return vnode;
  };

```

### sameVnode 函数

作用是比较两个 vnode 是否相同

```js
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;

  return isSameSel && isSameKey && isSameIs;
}
```

### patchVnode 函数

patchVnode 就是 patch 中用于更新差异化 DOM 的函数。

```ts
function patchVnode(
  oldVnode: VNode,
  vnode: VNode,
  insertedVnodeQueue: VNodeQueue
) {
  // 1.声明变量
  const hook = vnode.data?.hook;
  hook?.prepatch?.(oldVnode, vnode);
  const elm = (vnode.elm = oldVnode.elm)!;
  const oldCh = oldVnode.children as VNode[];
  const ch = vnode.children as VNode[];
  if (oldVnode === vnode) return;

  // 2.触发钩子函数
  if (vnode.data !== undefined) {
    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    vnode.data.hook?.update?.(oldVnode, vnode);
  }

  // 3.判断text是否未定义
  if (isUndef(vnode.text)) {
    // 3.1 如果新老节点都有children
    if (isDef(oldCh) && isDef(ch)) {
      // 使用diff算法对比子节点，更新子节点。
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);

      // 3.2 如果只有新节点有children
    } else if (isDef(ch)) {
      // 如果老节点有text的话，清空DOM元素的内容。
      if (isDef(oldVnode.text)) api.setTextContent(elm, "");
      // 批量添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 3.3 如果只有老节点有children
    } else if (isDef(oldCh)) {
      // 指移除子节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 3.4 老节点如果有text 清空它
    } else if (isDef(oldVnode.text)) {
      api.setTextContent(elm, "");
    }

    // 4.判断新老节点的text 是否相同
  } else if (oldVnode.text !== vnode.text) {
    if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    api.setTextContent(elm, vnode.text!);
  }

  // 5.执行用户设置的postpatch钩子函数
  hook?.postpatch?.(oldVnode, vnode);
}
```

### updateChildren 函数

updateChildren 函数就是 snabbdom 中处理新旧节点的 children diff 差异  
在比较时只会同层级进行比较，就像是第二层老节点的 C，在新节点中变成了 D，但是下面的子节点又相同，这个时候会直接把 C 及子节点删除掉，更换成老节点

![tree1](img/tree1.png)

上面的树在 js 中的结构展示

```ts
//老节点的结构
// h函数生成vnode   h("div", [h("div", [h("div", "B1"), h("div", "B1")], "B"), h("div", [h("div", "C1"), h("div", "C1")], "C")], "A");

{
    "sel": "div",
    "data": [
        {
            "sel": "div",
            "data": [
                {
                    "sel": "div",
                    "data": {},
                    "text": "B1"
                },
                {
                    "sel": "div",
                    "data": {},
                    "text": "B1"
                }
            ],
            "text": "B"
        },
        {
            "sel": "div",
            "data": [
                {
                    "sel": "div",
                    "data": {},
                    "text": "C1"
                },
                {
                    "sel": "div",
                    "data": {},
                    "text": "C1"
                }
            ],
            "text": "C"
        }
    ],
    "text": "A",
    "elm": {}
}

```

#### 例子：演示 diff 核心算法

##### 来看一下 **_ 同等级 _**vnode 是否相同，指针会对节点进行以下 4 种比较

1. ##### oldStartIdx & newStartIdx 老节点开始和新节点开始

2. ##### oldEndIdx & newEndIdx 老节点结束和新节点结束

3. ##### oldStartIdx & newEndIdx 老节点开始和新节点结束

4. ##### oldEndIdx & newStartIdx 老节点结束和新节点开始

<br />
<br />
<br />
<br />
<br />
<br />

假设有两个 vNode 节点，老节点的内容是 ABCD，新节点的内容是 ACED。让我们来看一下它们是怎么进行比较的。

![snabbdom 同等级进行比较](img/tree2.png)

**_ 步骤一 _**  
这个时候会对 oldStartIdx & newStartIdx 进行比较（sel 和 key 是否相同），相同的话直接用 patchVNode 对内容进行对比和更新,  
并把新旧开始节点指针++，对后面的节点进行比较。
![snabbdom 同等级进行比较](img/tree3.png)
**_ 步骤二 _**  
这时发现新旧开始节点不同，还不能确定不同的节点是新增的还是移动过来的，所以节点指针先保持不动，开始比较 oldEndIdx & newEndIdx （sel 和 key 是否相同），相同的话直接用 patchVNode 对内容进行对比和更新
并把新旧结束节点指针--，对前面的节点进行比较。

![snabbdom 同等级进行比较](img/tree4.png)

**_步骤三 _**
这时发现 oldEndIdx 和 newEndIdx 并不相同，采用第三种情况，oldStartIdx 和 newEndIdx 进行比较，发现也不相同。  
采用第四种情况 oldEndIdx 和 newStartIdx 进行比较,发现是相同的，会把 oldEndVnode 移动到 oldStartIdx 前面的位置，并把 oldEndIdx-- newStartIdx++

![snabbdom 同等级进行比较](img/tree5.png)

#### updateChildren 源码

```ts
function updateChildren(
  parentElm: Node,
  oldCh: VNode[],
  newCh: VNode[],
  insertedVnodeQueue: VNodeQueue
) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newEndIdx = newCh.length - 1;
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];
  let oldKeyToIdx: KeyToIndexMap | undefined;
  let idxInOld: number;
  let elmToMove: VNode;
  let before: any;

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 为空指针直接偏移，进行下一位比较。
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
      api.insertBefore(
        parentElm,
        oldStartVnode.elm!,
        api.nextSibling(oldEndVnode.elm!)
      );
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
      api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 如果两个节点对比时不为空 且4种情况都不符合，说明是新增或移动节点。

      // 先获取oldCh的 oldStartIdx 和 oldEndIdx 区间的元素Idx（老区间），从始至终只获取一次
      if (oldKeyToIdx === undefined) {
        // 获取old 的区间属性
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      // 从上面老区间 属性里查找newStartVnode 是否存在。
      // newStartVnode也作为移动节点的参考点
      idxInOld = oldKeyToIdx[newStartVnode.key as string];

      // undefined说明newStartVnode 是个新节点，进行插入操作。
      if (isUndef(idxInOld)) {
        // New element
        // 以 oldStartVnode 为参考点 插入新元素
        api.insertBefore(
          parentElm,
          createElm(newStartVnode, insertedVnodeQueue),
          oldStartVnode.elm!
        );
      } else {
        // 说明节点已存在，只是移动了位置。既然我有，就从oldCh中获取此节点
        elmToMove = oldCh[idxInOld];
        if (elmToMove.sel !== newStartVnode.sel) {
          // 如果元素不同，认为也是新节点插入
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        } else {
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); // 比较两个节点的差异化
          oldCh[idxInOld] = undefined as any; // 移动后，将此位置赋值undefined，后续再比较时，直接偏移跳过。

          // oldStartVnode
          api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          // 为什么以 oldStartVnode 为参考点
        }
      }

      // 这时新节点已判断出是新增或移动，进行指针右移，下一位
      newStartVnode = newCh[++newStartIdx];
    }
  }

  if (newStartIdx <= newEndIdx) {
    // 说明
    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
    addVnodes(
      parentElm,
      before,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    );
  }
  if (oldStartIdx <= oldEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}
```

index.js 换成如下内容查看 diff 算法

```js
// 示例代码1
import { init, h, thunk, vnode } from "snabbdom";

// patch 函数用于比较两个vnode的差异，渲染到DOM上
const patch = init([]);

// h 函数用于生成vnode
let vNode1 = h("ul", [
  h("li", { key: "a" }, "a"),
  h("li", { key: "b" }, "b"),
  h("li", { key: "c" }, "c"),
  h("li", { key: "d" }, "d"),
]);
let vNode2 = h("ul", [
  h("li", { key: "a" }, "a"),
  h("li", { key: "c" }, "c"),
  h("li", { key: "e" }, "e"),
  h("li", { key: "d" }, "d"),
]);

// let vNode2 = h("div", h("ul", [h("li", "list1"), h("li", "list2")]))

let app = document.querySelector("#app");

let newdata = patch(app, vNode1);
patch(newdata, vNode2);
```

#### updateChildren 总结

##### key 的作用

- 简单的说: key 是虚拟 DOM 对象的标识, 在更新显示(对比新旧节点)时 key 起着极其重要的作用。
- 比较规则如下：
  - 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key：若虚拟 DOM 中内容没变, 直接使用之前的真实 DOM。若虚拟 DOM 中内容变了, 则生成新的真实 DOM，随后替换掉页面中之前的真实 DOM。
  - 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key，则 react 或者 vue 会根据数据创建新的 DOM，随后渲染到到页面

##### 用 index 作为 key 可能会引发的问题：

- 若对数据进行：逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实 DOM 更新 ==> 界面效果没问题, 但效率低。
- 如果结构中还包含输入类的 DOM：会产生错误 DOM 更新 ==> 界面有问题。
- 注意！如果不存在对数据的逆序添加、逆序删除等破坏顺序操作，仅用于渲染列表用于展示，使用 index 作为 key 是没有问题的。

```js
import {
  init,
  h,
  thunk,
  vnode,
  styleModule,
  eventListenersModule,
} from "snabbdom";

// patch 函数用于比较两个vnode的差异，渲染到DOM上
const patch = init([styleModule, eventListenersModule]);

let num = 0;
let input = () => {
  return h("input", {
    key: num,
    style: {
      display: "block",
    },
  });
};
let c = [input()];

let vNode = (children = []) => {
  return h("div.#app", { key: "app" }, [
    h(
      "button",
      {
        style: {
          color: "green",
          display: "block",
        },
        on: {
          click: () => updateChildren(),
        },
      },
      "来 ~ 点我"
    ),

    h("div.box", children),
  ]);
};
patch(document.querySelector("#app"), vNode(c));

function updateChildren() {
  ++num;
  c.unshift(input());
  patch(document.querySelector("#app"), vNode(c));
}
// https://blog.csdn.net/pagnzong/article/details/118875840
```

:::tip
当数组中的 DOM 值发生变化时，如果使用 id（每条数据的唯一标识），发现新的值和原来的值没变，不需要重新渲染，只需要计算新增的，然后整体渲染就可以了。减少了重新计算的次数。

如果使用 index。中间如果插入了一个新元素。则新增元素后面的元素的下标都变化了，要重新计算新增元素以及新增元素后面的值，则重新渲染的虚拟 DOM 也就多了。
:::

## ts 类型

### VNodeData

```js
interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  is?: string; // for custom elements v1
  [key: string]: any; // for any other 3rd party module
}
```

### VNode

```js
export interface VNode {
  sel: string | undefined; // 元素名
  data: VNodeData | undefined; // 节点的样式、class、事件等
  children: Array<VNode | string> | undefined;
  elm: Node | undefined;
  text: string | undefined;
  key: Key | undefined; // 优化使用
}
```

## vue2 为什么选择 双指针算法。

- 效率：双指针算法是一种相对高效的算法，因为它只需要对比新旧 VNode 树的节点一次，而不需要对每个节点都进行比较。
- 简单性：双指针算法相对简单易懂，不需要复杂的数据结构和算法，容易实现和调试。
- 便于扩展：双指针算法可以方便地扩展到嵌套的 VNode 树中，因为它可以处理任意深度的嵌套节点。
- 维护 Virtual DOM：Vue 2 中使用 Virtual DOM 的主要目的是为了简化 DOM 操作和提高性能，双指针算法能够较好地维护 Virtual DOM 的更新，从而实现这个目的。

总之，双指针算法在当时的场景下是一种比较合适的算法选择。但是，在 Vue 3 中，由于使用了 Proxy 对象来代理响应式对象，Vue 可以更加精确地追踪数据的变化，因此 diff 算法可以更加高效和准确，所以采用了递归/静态分析算法。

## vue2 vue3 diff 算法区别

### 对比流程不同

Vue 3 中，当一个组件的状态发生变化时，Vue 会先对组件进行“静态分析”，即分析组件的模板和数据，生成一个优化后的渲染函数。
然后，Vue 会将新旧渲染函数进行逐层递归比较，找出需要更新的部分，并进行更新。这种算法相比于双指针算法，可以更加高效地找出需要更新的部分，并且可以减少不必要的 DOM 操作，提高性能。

<!-- Vue 3 中的虚拟 DOM 模块使用了和 Vue 2 不同的 diff 算法，下面是 Vue 3 中的 diff 算法的主要流程：

- 首先，比较新旧 VNode 的类型，如果类型不同，直接替换节点。如果类型相同，进行下一步比较。
- 然后，比较新旧 VNode 的 key 值，如果 key 值不同，则说明两个节点不同，直接替换节点。如果 key 值相同，进行下一步比较。
- 接着，比较新旧 VNode 的标签名和命名空间是否相同，如果不同，则说明两个节点不同，直接替换节点。如果相同，进行下一步比较。
- 然后，比较新旧 VNode 的属性和事件是否相同，如果不同，则更新属性和事件。如果相同，进行下一步比较。
- 接着，比较新旧 VNode 的子节点是否相同，如果不同，则递归进行 diff，找出新增、删除和移动的节点，执行相应的操作。
- 最后，将新节点的属性和事件设置到真实 DOM 中，完成整个 diff 过程。

总之，Vue 3 中的 diff 算法主要是通过比较新旧 VNode 的类型、key 值、标签名、命名空间、属性、事件和子节点来判断节点是否需要更新或替换。如果需要更新或替换，就执行相应的操作。如果需要递归 diff 子节点，则重复执行上述流程。相较于 Vue 2 的 diff 算法，Vue 3 的 diff 算法更加高效，因为它使用了类似于双指针的算法，减少了比较的次数，从而提高了 diff 的速度。 -->

## 什么时候会触发diff算法

  - vue2
    - 页面上的数据变化时。
    - router 切换页面时。
  - vue3 
    - 页面上的数据变化时。
    - router 切换页面时不会触发diff。

### vue3 优化内容

Vue 2 的 diff 算法使用的是双指针算法，而 Vue 3 并没有使用双指针算法而是采用的是基于模板的优化算法(称为“递归/静态分析”算法)。

Vue 3 的 diff 算法主要是通过模板编译阶段对模板的静态分析和优化，以及在渲染阶段对动态节点的跟踪和处理来实现的。主要包括以下两个方面的优化：

1.静态节点提升：Vue 3 的模板编译器会将模板中的静态节点（即不会变化的节点）提升到渲染函数的外部，这样可以减少每次渲染时对这些节点的处理，提高渲染效率。  
2.模板的动态更新：在渲染过程中，Vue 3 的 diff 算法主要关注动态节点的处理，即不再需要对同一层级的所有节点进行比较。Vue 3 使用了一种称为“递归动态更新”的算法，在渲染过程中对动态节点进行跟踪和处理，减少了不必要的 DOM 操作，从而提高了渲染效率。具体来说，递归动态更新算法会先将新旧 VNode 树进行同层级比较，尽可能地复用已有节点，然后对比新旧 VNode 树的子节点，依次进行处理。如果子节点相同，将跳过该节点的处理；如果子节点不同，将重新创建节点并进行替换操作，如果子节点在新 VNode 树中不存在，将进行删除操作，如果子节点在旧 VNode 树中不存在，将进行插入操作。

相比于 Vue 2 的双指针算法，Vue 3 的基于模板的优化算法在静态节点处理和动态节点更新方面都进行了优化，提高了渲染效率和性能。

## vue 和 react diff 算法区别

React 和 Vue 都使用了虚拟 DOM 来提高性能，并采用了不同的 Diff 算法来比较新旧虚拟 DOM 之间的差异并更新页面。下面是 React Diff 算法和 Vue Diff 算法的一些区别：

### 比较方式不同

- React 采用的是自顶向下的递归算法，会逐层递归比较子节点。而 Vue Diff 算法采用的是双端比较的算法，会同时从新旧虚拟 DOM 的头部和尾部开始比较，以尽早地发现差异。
- React 算法采用的是按照节点类型和顺序来比较的策略，而 Vue Diff 算法则采用的是按照节点类型和 key 值来比较的策略。React Diff 算法会先比较节点类型和顺序，然后再比较节点的 key 值；而 Vue Diff 算法则会先比较节点类型和 key 值，然后再比较节点的顺序。

### 优化策略不同

- React 算法采用了一些特殊的优化策略，例如可以利用组件的 shouldComponentUpdate 生命周期钩子函数来避免不必要的渲染。
- Vue Diff 算法则使用了一些较为高级的优化策略，例如使用了动态规划算法来处理较大的虚拟 DOM 树。

总的来说，React 和 Vue 的 Diff 算法都是为了提高虚拟 DOM 的渲染性能而设计的，它们的实现方式和优化策略有所不同，但都是非常有效的。在实际开发中，根据具体的应用场景和需求来选择合适的框架和算法是非常重要的。
